package com.stargis.ftpdown.util;

import java.io.*;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.stream.Collectors;

import cn.hutool.core.util.ObjectUtil;
import com.jcraft.jsch.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * sftp工具。注意：构造方法有两个：分别是基于密码认证、基于秘钥认证。
 *
 * @author Somnus
 */
public class SFTPUtil {
    private static final Logger LOG = LoggerFactory.getLogger(SFTPUtil.class);
    private ChannelSftp sftp;
    private String userName; // FTP 登录用户名
    private String password; // FTP 登录密码
    private String keyFilePath;// 私钥文件的路径
    private String host; // FTP 服务器地址IP地址
    private int port; // FTP 端口
    private Session sshSession;

    /**
     * 构造基于密码认证的sftp对象
     *
     * @param userName
     *            用户名
     * @param password
     *            登陆密码
     * @param host
     *            服务器ip
     * @param port
     *            fwq端口
     */
    public SFTPUtil(String userName, String password, String host, int port) {
        super();
        this.userName = userName;
        this.password = password;
        this.host = host;
        this.port = port;
    }

    /**
     * 构造基于秘钥认证的sftp对象
     *
     * @param userName
     *            用户名
     * @param host
     *            服务器ip
     * @param port
     *            fwq端口
     * @param keyFilePath
     *            私钥文件路径
     */
    public SFTPUtil(String userName, String host, int port, String keyFilePath) {
        super();
        this.userName = userName;
        this.host = host;
        this.port = port;
        this.keyFilePath = keyFilePath;
    }

    /**
     * 连接sftp服务器
     *
     * @throws Exception
     */
    public void connect() throws Exception {
        try {
            JSch jsch = new JSch();
            if (keyFilePath != null) {
                jsch.addIdentity(keyFilePath);// 设置私钥
                LOG.info("连接sftp，私钥文件路径：" + keyFilePath);
            }
            LOG.info("sftp host: " + host + "; userName:" + userName);

            sshSession = jsch.getSession(userName, host, port);
            LOG.debug("Session 已建立.");
            if (password != null) {
                sshSession.setPassword(password);
            }
            Properties sshConfig = new Properties();
            sshConfig.put("StrictHostKeyChecking", "no");
            sshSession.setConfig(sshConfig);
            sshSession.connect();
            LOG.debug("Session 已连接.");
            Channel channel = sshSession.openChannel("sftp");
            channel.connect();

            sftp = (ChannelSftp) channel;
            LOG.info("连接到SFTP成功。host: " + host);
        } catch (Exception e) {
            LOG.error("连接sftp失败！", e);
            throw e;
        }
    }

    /**
     * 关闭连接 server
     */
    public void disconnect() {
        if (sftp != null) {
            if (sftp.isConnected()) {
                sftp.disconnect();
                sshSession.disconnect();
                LOG.info("sftp连接关闭成功！" + sftp);
            } else if (sftp.isClosed()) {
                LOG.warn("sftp 已经关闭,不需要重复关闭！" + sftp);
            }
        }

    }

    /**
     * 将输入流的数据上传到sftp作为文件
     *
     * @param directory
     *            上传到该目录
     * @param sftpFileName
     *            sftp端文件名
     * @param input
     *            输入流
     * @throws Exception
     */
    public void upload(String directory, String sftpFileName, InputStream input) throws Exception {
        try {

            try {// 如果cd报异常，说明目录不存在，就创建目录
                sftp.cd(directory);
            } catch (Exception e) {
                sftp.mkdir(directory);
                sftp.cd(directory);
            }
            sftp.put(input, sftpFileName);
            LOG.info("sftp上传成功！文件名：" + sftpFileName);
        } catch (Exception e) {
            LOG.error("sftp上传失败！文件名" + sftpFileName, e);
            throw e;
        }
    }

    /**
     * 上传单个文件
     *
     * @param directory
     *            上传到sftp目录
     * @param uploadFile
     *            要上传的文件,包括路径
     * @throws Exception
     */
    public void upload(String directory, String uploadFile) throws Exception {
        File file = new File(uploadFile);
        upload(directory, file.getName(), new FileInputStream(file));
    }

    /**
     * 将byte[]上传到sftp，作为文件。注意:从String生成byte[]是，要指定字符集。
     *
     * @param directory
     *            上传到sftp目录
     * @param sftpFileName
     *            文件在sftp端的命名
     * @param byteArr
     *            要上传的字节数组
     * @throws Exception
     */
    public void upload(String directory, String sftpFileName, byte[] byteArr) throws Exception {
        upload(directory, sftpFileName, new ByteArrayInputStream(byteArr));
    }

    /**
     * 将字符串按照指定的字符编码上传到sftp
     *
     * @param directory
     *            上传到sftp目录
     * @param sftpFileName
     *            文件在sftp端的命名
     * @param dataStr
     *            待上传的数据
     * @param charsetName
     *            sftp上的文件，按该字符编码保存
     * @throws Exception
     */
    public void upload(String directory, String sftpFileName, String dataStr, String charsetName) throws Exception{
        upload(directory, sftpFileName, new ByteArrayInputStream(dataStr.getBytes(charsetName)));

    }

    /**
     * 下载文件
     *
     * @param directory
     *            下载目录
     * @param downloadFile
     *            下载的文件
     * @param saveFile
     *            存在本地的路径
     * @throws Exception
     */
    public void download(String directory, String downloadFile, String saveFile) throws Exception {
        try {
            if (directory != null && !"".equals(directory)) {
                sftp.cd(directory);
            }
            File file = new File(saveFile);
            sftp.get(downloadFile, new FileOutputStream(file));
            LOG.info("sftp下载文件成功！文件名" + downloadFile);
        } catch (Exception e) {

            LOG.error("sftp下载文件失败！文件名：" + downloadFile, e);
            throw e;
        }
    }
    /**
     * 下载文件
     * @param directory 下载目录
     * @param downloadFile 下载的文件名
     * @return 字节数组
     * @throws Exception
     */
    public byte[] download(String directory, String downloadFile) throws Exception{
        byte[] fileData = null;
        try {
            if (directory != null && !"".equals(directory)) {
                sftp.cd(directory);
            }
            InputStream is = sftp.get(downloadFile);

            fileData = new byte[is.available()];
            is.read(fileData);

            LOG.info("sftp下载文件成功！文件名" + downloadFile);
        } catch (SftpException e) {

            LOG.error("sftp下载文件失败！文件名：" + downloadFile, e);
            throw e;
        } catch (IOException e) {
            LOG.error("sftp下载文件读取失败！文件名：" + downloadFile, e);
            throw e;
        }
        return fileData;
    }

    /**
     * 删除文件
     *
     * @param directory
     *            要删除文件所在目录
     * @param deleteFile
     *            要删除的文件
     * @throws Exception
     */
    public void delete(String directory, String deleteFile) throws Exception {
        try {
            sftp.cd(directory);
            sftp.rm(deleteFile);
        } catch (Exception e) {
            LOG.error("sftp删除文件失败" + deleteFile, e);
            throw e;
        }
    }

    /**
     * 列出目录下的文件
     *
     * @param directory
     * @return
     * @throws SftpException
     */
    public Vector listFiles(String directory) throws SftpException {
        return sftp.ls(directory);
    }

    /**
     * 判断远程目录是否存在
     *
     * @param remotePath 远程目录
     * @return true:存在 false:不存在
     */
    public boolean remoteDirectoryExists(String remotePath) {
        try {
            String handleFilePath = handlePath(remotePath, "");
            SftpATTRS attrs = sftp.lstat(handleFilePath);
            return attrs != null && attrs.isDir();
        } catch (SftpException e) {
            if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
                return false;
            } else {
                throw new RuntimeException("Error checking remote directory existence: " + e.getMessage(), e);
            }
        }
    }

    /**
     * 判断远程文件是否存在
     *
     * @param filePath 远程文件
     * @return true:存在 false:不存在
     */
    public boolean remoteFileExists(String filePath) {
        try {
            String handleFilePath = handlePath(filePath, "");
            if (handleFilePath.endsWith("/")) {
                handleFilePath = handleFilePath.substring(0, handleFilePath.length() - 1);
            }
            SftpATTRS attrs = sftp.stat(handleFilePath);
            return attrs != null && !attrs.isDir();
        } catch (SftpException e) {
            if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
                return false;
            } else {
                throw new RuntimeException("Error checking remote File existence: " + e.getMessage(), e);
            }
        }
    }

    /**
     * 判断远程路径是否为目录
     *
     * @param path 路径
     */
    public boolean remotePathIsDir(String path){
        try {
            String handleFilePath = handlePath(path, "");
            if (handleFilePath.endsWith("/")) {
                handleFilePath = handleFilePath.substring(0, handleFilePath.length() - 1);
            }
            SftpATTRS attrs = sftp.stat(handleFilePath);
            if (attrs != null) {
                if (attrs.isDir()) {
                    return true;
                }
            }
        } catch (SftpException e) {
            throw new RuntimeException("Error checking remote path existence: " + e.getMessage(), e);
        }
        return false;
    }
    /**
     * 路径处理
     *
     * @param path 路径
     * @param fileName 文件名
     */
    public static String handlePath(String path, String fileName) {
        String str;
        str = path.replace("\\", "/");
        if (str.endsWith("/")) {
            str = str + fileName;
        } else {
            str = str + "/" + fileName;
        }

        if (path.charAt(0) != '/') {
            str = "/" + str;
        }
        return str;
    }

    /**
     * 递归创建目录
     *
     * @param remotePath 目录
     * @throws SftpException 异常
     */
    private void createRemoteDirectoriesRecursively(String remotePath) throws SftpException {
        // 分割路径以获取各级目录名
        String[] directories = remotePath.split("/");

        // 过滤掉空字符串，因为split处理绝对路径时会包含一个空元素
        List<String> validDirectories = Arrays.stream(directories)
                .filter(dir -> !dir.isEmpty())
                .collect(Collectors.toList());

        // 用于构建当前路径的StringBuilder
        StringBuilder currentPath = new StringBuilder();

        //在开头添加/代表从跟节点开始创建目录
        currentPath.append("/");
        // 遍历所有层级的目录名
        for (String dir : validDirectories) {

            // 添加目录名到当前路径，并以"/"分隔
            currentPath.append(dir).append("/");

            //:则代表磁盘路径，跳过
            if(dir.contains(":")) continue;

            // 构建完整的路径并检查是否存在
            String fullPath = currentPath.toString();
            if (!remoteDirectoryExists(fullPath)) {
                // 如果不存在，则创建该层级的目录
                sftp.mkdir(fullPath);
            }
        }
    }

    /**
     * 文件传输
     *
     * @param sourcePath 源目录
     * @param targetPath 目标目录
     * @param closeSource 传输完成后是否关闭资源
     */
    public void transfer(String sourcePath, String targetPath, boolean closeSource) {
        transfer(sourcePath, targetPath, null, closeSource);
    }

    /**
     * 文件传输
     *
     * @param sourcePath 源目录
     * @param targetPath 目标目录
     * @param ignoredPathList 忽略路径
     * @param closeSource 传输完成后是否关闭资源
     */
    public void transfer(String sourcePath, String targetPath, List<String> ignoredPathList, boolean closeSource) {
        if(sourcePath == null || targetPath == null) {
            throw new IllegalArgumentException("源路径和目标路径不能为空");
        }
        try {
            uploadDirectory(sourcePath, targetPath, ignoredPathList);
            LOG.info("文件传输成功");
        } catch (Exception e) {
            LOG.error("文件传输失败：{}", e.getMessage());
            throw new RuntimeException("文件传输失败：" + e.getMessage(), e);
        } finally {
            if(closeSource) {
                // 资源泄露防护
                destroy();
            }
        }
    }

    /**
     * 文件传输
     *
     * @param sourcePath 源目录
     * @param targetPath 目标目录
     * @param startTime 源目录文件开始时间
     * @param endTime 源目录文件结束时间
     * @param closeSource 传输完成后是否关闭资源
     */
    public void transfer(String sourcePath, String targetPath, LocalDateTime startTime, LocalDateTime endTime, boolean closeSource) {
        transfer(sourcePath, targetPath, startTime, endTime, null, closeSource);
    }

    public void transfer(String sourcePath, String targetPath, LocalDateTime startTime, LocalDateTime endTime, List<String> ignoredPathList, boolean closeSource) {
        if(sourcePath == null || targetPath == null) {
            throw new IllegalArgumentException("源路径和目标路径不能为空");
        }
        try {
            uploadDirectory(sourcePath, targetPath, startTime, endTime, ignoredPathList);
            LOG.info("文件传输成功, 传输文件时间段{}-{}", startTime, endTime);
        } catch (Exception e) {
            LOG.error("文件传输失败：{}", e.getMessage());
            throw new RuntimeException("文件传输失败：" + e.getMessage(), e);
        } finally {
            if(closeSource) {
                // 资源泄露防护
                destroy();
            }
        }
    }

    /**
     * 保存inputStream文件到远程
     *
     * @param inputStream 流
     * @param remotePath 远程路径
     * @param remoteFileName 文件名
     * @param closeSource 是否关闭资源
     */
    public void uploadInputStreamToRemote(InputStream inputStream, String remotePath, String remoteFileName, boolean closeSource) {
        try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()){
            byte[] buffer = new byte[1024];
            int read;
            while ((read = inputStream.read(buffer)) != -1) {
                byteArrayOutputStream.write(buffer, 0, read);
            }
            byte[] fileContent = byteArrayOutputStream.toByteArray();
            String dirPath = handlePath(remotePath, "");
            sftp.cd(dirPath);
            OutputStream outputStream = sftp.put(remoteFileName);
            outputStream.write(fileContent);
            outputStream.flush();
            outputStream.close();

        } catch (Exception e) {
            LOG.error("文件传输失败：{}", e.getMessage());
            throw new RuntimeException("文件传输失败：" + e.getMessage(), e);
        }finally {
            if(closeSource) {
                // 资源泄露防护
                destroy();
            }
        }
    }


    /**
     * 销毁连接
     */
    public void destroy() {
        if (sftp != null) {
            try {
                sftp.disconnect();
            } catch (Exception e) {
                LOG.error("关闭SFTP通道失败：", e);
            }
        }
        if (sshSession != null) {
            sshSession.disconnect();
        }
    }


    /**
     * 上传目录
     *
     * @param localDirPath 本地目录
     * @param remoteBasePath 远程目录
     */
    private  void uploadDirectory(String localDirPath, String remoteBasePath)  {
        uploadDirectory(localDirPath, remoteBasePath, null);
    }

    /**
     * 上传目录
     *
     * @param localDirPath 本地目录
     * @param remoteBasePath 远程目录
     * @param ignoredFileList 忽略文件
     */
    private  void uploadDirectory(String localDirPath, String remoteBasePath, List<String> ignoredFileList)  {
        File localDir = new File(localDirPath);
        if (!localDir.exists() || !localDir.isDirectory()) {
            throw new IllegalArgumentException("Local path: " + localDirPath + " is not a valid directory.");
        }

        try {
            String remotePath = handlePath(remoteBasePath, localDir.getName());
            //目录不存在创建目录
            createRemoteDirectoriesRecursively(remotePath);

            sftp.cd(remotePath);

            File[] files = localDir.listFiles();
            if (files != null) {
                if(ObjectUtil.isNotEmpty(ignoredFileList)){
                    files = Arrays.stream(files).filter(file -> !ignoredFileList.contains(file.getAbsolutePath())).toArray(File[]::new);
                }
                for (File file : files) {
                    if (file.isDirectory()) {
                        uploadDirectory(file.getAbsolutePath(), remotePath, ignoredFileList);
                    } else {
                        String remoteFilePath = handlePath(remotePath, file.getName());
                        sftp.put(file.getAbsolutePath(), remoteFilePath);
                    }
                }
            }
        } catch (SftpException e) {
            throw new RuntimeException("Error during SFTP operation: " + e.getMessage(), e);
        }
    }

    /**
     * 上传目录
     *
     * @param localDirPath 本地目录
     * @param remoteBasePath 远程目录
     * @param startTime 文件开始时间
     * @param endTime 文件结束时间
     */
    private  void uploadDirectory(String localDirPath, String remoteBasePath, LocalDateTime startTime, LocalDateTime endTime)  {
        uploadDirectory(localDirPath, remoteBasePath, startTime, endTime, null);
    }

    /**
     * 上传目录
     *
     * @param localDirPath 本地目录
     * @param remoteBasePath 远程目录
     * @param startTime 文件开始时间
     * @param endTime 文件结束时间
     * @param ignoredPathList 忽略文件
     */
    private  void uploadDirectory(String localDirPath, String remoteBasePath, LocalDateTime startTime, LocalDateTime endTime, List<String> ignoredPathList)  {
        File localDir = new File(localDirPath);
        if (!localDir.exists() || !localDir.isDirectory()) {
            throw new IllegalArgumentException("Local path: " + localDirPath + " is not a valid directory.");
        }

        try {
            String remotePath = handlePath(remoteBasePath, localDir.getName());
            //目录不存在创建目录
            createRemoteDirectoriesRecursively(remotePath);

            sftp.cd(remotePath);

            File[] files = localDir.listFiles((dir1, name) -> {
                File file = new File(dir1, name);
                ZoneId zoneId = ZoneId.systemDefault();
                if (startTime != null && endTime != null) {
                    return file.lastModified() >= startTime.atZone(zoneId).toInstant().toEpochMilli()
                            && file.lastModified() <= endTime.atZone(zoneId).toInstant().toEpochMilli();
                }else if (startTime != null) {
                    return file.lastModified() >= startTime.atZone(zoneId).toInstant().toEpochMilli();
                }
                return file.lastModified() <= endTime.atZone(zoneId).toInstant().toEpochMilli();
            });
            if (files != null) {
                if(ObjectUtil.isNotEmpty(ignoredPathList)){
                    files = Arrays.stream(files).filter(file -> !ignoredPathList.contains(file.getAbsolutePath())).toArray(File[]::new);
                }
                for (File file : files) {
                    if (file.isDirectory()) {
                        uploadDirectory(file.getAbsolutePath(), remotePath, startTime, endTime, ignoredPathList);
                    } else {
                        String remoteFilePath = handlePath(remotePath, file.getName());
                        sftp.put(file.getAbsolutePath(), remoteFilePath);
                    }
                }
            }
        } catch (SftpException e) {
            throw new RuntimeException("Error during SFTP operation: " + e.getMessage(), e);
        }
    }

    /**
     * 上传文件
     *
     * @param localDirPath 本地路径
     * @param remoteBasePath 远程路径
     */
    public void uploadFile(String localDirPath, String remoteBasePath)  {
        File file = new File(localDirPath);
        if (!file.exists() || !file.isFile()) {
            throw new IllegalArgumentException("Local path: " + localDirPath + " is not a valid file.");
        }

        try{
            String remotePath = handlePath(remoteBasePath, "");
            //目录不存在创建目录
            createRemoteDirectoriesRecursively(remotePath);

            sftp.cd(remotePath);

            String remoteFilePath = handlePath(remoteBasePath, file.getName());
            sftp.put(localDirPath, remoteFilePath);
        } catch (SftpException e) {
            throw new RuntimeException("Error during SFTP operation: " + e.getMessage(), e);
        }
    }

    /**
     * 执行远程windows主机命令
     *
     * @param cmd 命令
     * @throws JSchException 异常
     * @throws IOException 异常
     */
    public int executeCmdForWindow(String cmd) throws JSchException, IOException {
        Channel channel = sshSession.openChannel("exec");
        LOG.info("执行命令：" + cmd);
        ((ChannelExec) channel).setCommand("cmd.exe /c " + cmd);

        // 获取输出流和错误流以便读取命令执行结果
        InputStream in = channel.getInputStream();
        InputStream err = ((ChannelExec) channel).getErrStream();

        channel.connect();
        StringBuilder outputSb = new StringBuilder();
        byte[] buffer = new byte[1024];
        int status = 0;
        while (true) {
            while (in.available() > 0) {
                int i = in.read(buffer, 0, 1024);
                if (i < 0)
                    break;
                outputSb.append(new String(buffer, 0, i));
            }
            while (err.available() > 0) {
                int i = err.read(buffer, 0, 1024);
                if (i < 0)
                    break;
                outputSb.append(new String(buffer, 0, i));
            }
            if (channel.isClosed()) {
                if (in.available() > 0) continue;
                status = channel.getExitStatus();
                break;
            }
        }
        LOG.info("执行结果：" + outputSb);
        channel.disconnect();

        return status;
    }

    /**
     * 执行远程linux主机命令
     *
     * @param cmd 命令
     * @throws JSchException 异常
     * @throws IOException 异常
     */
    public int executeCmdForLinux(String cmd) throws JSchException, IOException {
        Channel channel = sshSession.openChannel("shell");
        // 获取输入流和输出流以交互
        OutputStream input = channel.getOutputStream();
        PrintStream ps = new PrintStream(input, true);

        channel.connect();
        LOG.info("执行命令：" + cmd);
        ps.println(cmd + "; echo $?"); //添加 "; echo $?" 来获取命令的退出状态码

        BufferedReader reader = new BufferedReader(new InputStreamReader(channel.getInputStream()));
        String line;
        StringBuilder outputBuilder = new StringBuilder();
        while ((line = reader.readLine()) != null) {
            outputBuilder.append(line).append("\n");
            // 当读取到退出状态码时，跳出循环
            if (line.trim().matches("\\d+")) {
                break;
            }
        }
        String[] outArray = outputBuilder.toString().trim().split("\n");
        int exitStatus = Integer.parseInt(outArray[outArray.length - 1]);

        // 判断命令是否成功执行
        if (exitStatus == 0) {
            LOG.info("命令执行成功");
        } else {
            LOG.error("命令执行失败，退出状态码: " + outputBuilder.toString());
        }

        reader.close();
        ps.close();
        channel.disconnect();

        return exitStatus;
    }


    /**
     * 遍历远程文件
     *
     * @param remotePath
     * @return
     * @throws Exception
     */
    public List<String> listRemoteFiles(String remotePath) {
        List<String> ftpFileNameList = new ArrayList<String>();
        ChannelSftp.LsEntry isEntity = null;
        String fileName = null;
        try {
            Vector<ChannelSftp.LsEntry> sftpFile = sftp.ls(remotePath);
            Iterator<ChannelSftp.LsEntry> sftpFileNames = sftpFile.iterator();
            while (sftpFileNames.hasNext()) {
                isEntity = (ChannelSftp.LsEntry) sftpFileNames.next();
                fileName = isEntity.getFilename();
                if(!fileName.equals(".") && !fileName.equals("..")) {
                    ftpFileNameList.add(fileName);
                }
            }
            return ftpFileNameList;
        } catch (Exception e) {
            LOG.error("遍历查询sftp服务器上文件异常", e);
            return null;
        }

    }

}